# 数据加密 crypto
30 秒速记
- 先分清要解决什么:保密用加密、防篡改用
HMAC/ 签名、存密码用慢哈希、校验内容用摘要 - 随机值用
crypto.randomBytes/crypto.randomUUID,Math.random不能拿来生成token、密钥 - 密码存储用
scrypt/bcrypt/Argon2加随机盐,不要直接MD5/SHA-256 - 对称加密优先
AES-256-GCM这类带认证的模式,每条消息一个新IV createCipher/createDecipher已废弃,Node 22删除,统一用createCipheriv
crypto 模块是 Node 对 OpenSSL 的封装,用好它的前提是先想清楚你要的是保密、防篡改、证明身份还是存密码,这四件事用的工具完全不同。 摘要(createHash)只能看出内容变没变;HMAC 多了一把共享密钥,能证明是自己人生成的;数字签名用私钥签、公钥验,能证明是谁发的;加密(createCipheriv)才是让别人看不懂。存密码又是另一回事,要用 scrypt 这种故意算得很慢的函数。面试里最常见的错误回答就是把这几个混成「加密」,比如说「密码用 MD5 加密」,这句话本身就有两处错。
理解「数据加密 crypto」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 security 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
一张表记住 crypto 里几类能力分别解决什么问题:
| 需求 | 工具 | Node 写法 |
|---|---|---|
| 判断内容有没有变 | 摘要 | crypto.createHash('sha256') |
| 证明消息是持有共享密钥的人发的 | HMAC |
crypto.createHmac('sha256', key) |
| 证明消息是某个私钥持有人发的 | 数字签名 | crypto.sign / crypto.verify |
| 让别人看不懂 | 对称加密 | crypto.createCipheriv('aes-256-gcm', key, iv) |
| 双方在公开信道上协商密钥 | 密钥交换 | crypto.createECDH / crypto.diffieHellman |
| 存用户密码 | 慢哈希 | crypto.scrypt |
生成随机 token / 密钥 |
安全随机数 | crypto.randomBytes / crypto.randomUUID |
一个能直接跑的密码存储例子:
const crypto = require('crypto');
function hashPassword(password) {
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64);
return salt.toString('hex') + ':' + hash.toString('hex');
}
function checkPassword(password, stored) {
const [saltHex, hashHex] = stored.split(':');
const hash = crypto.scryptSync(password, Buffer.from(saltHex, 'hex'), 64);
// 用 timingSafeEqual 比较,避免按字节提前退出带来的时序泄露
return crypto.timingSafeEqual(hash, Buffer.from(hashHex, 'hex'));
}
const stored = hashPassword('123456');
console.log(checkPassword('123456', stored)); // true
console.log(checkPassword('654321', stored)); // false
记住两件事就不会出大错:密码不加密只做慢哈希,比较哈希值用 timingSafeEqual 不用 ===。
面试官追问
追问 1同事说「接口用了 crypto 模块,所以是安全的」,你怎么回?
用了模块不代表用对了。要看具体调了什么:拿 MD5 存密码、AES 用固定 IV、用 Math.random 生成密钥,都是「用了 crypto」但一点都不安全。
追问 2生成重置密码的 token,Math.random().toString(36) 行不行?
不行,Math.random 不是密码学安全的随机数,输出可以被预测。用 crypto.randomBytes(32).toString('hex'),或者 crypto.randomUUID()。
追问 3用户密码入库,sha256(password + salt) 够不够?
不够。SHA-256 太快了,显卡一秒能算几十亿次,弱密码很快就被跑出来。用 crypto.scrypt(password, salt, 64, cb) 这类专门的慢哈希,盐每个用户随机生成一份。
追问 4加密结果要能防篡改,AES-CBC 加完就够了吗?
不够,CBC 只管保密,密文被改了解出来就是乱码,不会报错。换成 aes-256-gcm,解密时校验 authTag,被改过直接抛异常;或者 CBC 后面再补一层 HMAC。
追问 5项目里搜出了 crypto.createCipher,要不要改?
要改。它从 Node 10 起废弃,Node 22 已经删掉了,升级 Node 直接报错。而且它用 MD5 从密码推 key 和 IV,同一个密码每次 IV 都一样,换成 scrypt 派生 key 加 createCipheriv。
# 写在前面
30 秒速记
- 这一章按「摘要 →
HMAC→ 加解密 → 签名 → 密钥交换 → 证书」的顺序走,难度递增 - 每个
API都看三件事:输入是什么编码、update能不能多次调、final/digest之后对象就作废 - 示例里出现的
createCipher、createDecipher是旧API,Node 22已删,只当历史了解 - 版本差异看清楚:
crypto.randomUUID要Node 14.17+,一次性crypto.hash()要Node 21.7+/20.12+
这一章是 crypto 模块的使用笔记,读的时候抓住一条主线:每种能力都是「创建对象 → 多次 update 喂数据 → 一次 digest / final 收尾」。 哈希、HMAC、加密、签名全是这个套路,收尾之后对象就不能再用,要重新算就重新 create。另一个要带着看的是版本:文中有些示例写于 Node 6 时代,比如 createCipher,现在已经废弃并在 Node 22 删除,学原理没问题,写新代码要换成 createCipheriv。编码也要时刻留意,输入是 utf8 字符串还是 Buffer,输出要 hex 还是 base64,两端对不上是最常见的「算出来不一样」。
本章节写得差不多了,不过还需要再整理一下(TODO)。
理解「写在前面」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1hash、hmac、cipher、sign 这几个对象用法有什么共同点?
都是 create 出来后 update 若干次,最后 digest / final / sign 一次收尾。收尾之后对象作废,再调会报错,而且它们都是 Transform 流,可以直接 pipe。
追问 2网上教程里的 crypto.createCipher('aes192', pwd) 能照着抄吗?
别抄。这个 API 在 Node 22 已经没了,而且它推导出的 IV 每次都一样,不安全。新代码用 createCipheriv,key 用 scrypt 派生或者随机生成。
追问 3同一段文本,Node 和浏览器算出来的摘要不一样,先怀疑什么?
先怀疑输入字节不一样,而不是算法。中文的编码、换行符 \r\n 和 \n、末尾有没有空格,任何一个字节不同摘要就完全不同。
追问 4示例想在 CI 里跑起来当回归测试,最少要做到什么?
把输入固定成文件或常量,期望值写死在断言里,比如 sha256('hello') 的结果;出错路径要让进程非零退出。只截一张成功的终端截图没有任何约束力。
# hash例子
30 秒速记
- 套路:
createHash('sha256')→update()可以调多次 →digest('hex')收尾 digest传hex/base64返回字符串,不传返回Bufferdigest之后对象作废,再update/digest抛Digest already called- 大文件别
readFileSync,用流:
